package oop.ex3.filemanager;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;

import oop.ex3.sharedclasses.IPaddress;;


//TODO complete JavaDoc when done programming
/**
 * Represents a FileManager.
 * Handles the database of the FileManager,
 * @author zablo
 *
 */
public class FileManager {
	
	private static final int TIMEOUT_5_SECONDS = 5000;
	
	private TreeSet<UploadFile> _files; // the FileManager's fileNames
	private LinkedList<IPaddress> _nameServers; // the FileManager's nameServers
	private String _directory; // the FileManager's directory
	private IPaddress _ipAddress; // the FileManager's full IPaddress
	
	private ServerSocket _requestSocket; //will listen to upload requests.
	//private LinkedList<UploadThread> _uploads;
	//TODO check moving for Concurrent...
	private ConcurrentLinkedQueue<UploadThread> _uploads;
	// maps a linkedList of uploadThreads uploading an UploadFile to the UploadFile.
	
	

	
	/**
	 * Constructs a new FileManager.
	 * Creates all the FileManager's databases - 
	 * server list, files tree, directory, IPaddress.
	 * also starts a ServerPort used to listen to upload requests.
	 * @param serverList the path of the serverList file.
	 * @param directory the path of the FileManager's directory.
	 * @param port the FileManager's communication port.
	 */
	public FileManager(String serverList, String directory, int port){
		
		try {
			this._ipAddress = new IPaddress(InetAddress.getLocalHost().getHostAddress(),
																	port);
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			//can be ignored
			e.printStackTrace();
		}
		
		this._nameServers=new LinkedList<IPaddress>();
		createNameServerList(serverList);
		
		this._directory=directory;
		this._files=new TreeSet<UploadFile>();
		createFilesTree();
		
		this._uploads = new ConcurrentLinkedQueue<UploadThread>();
		
		try {
			_requestSocket = new ServerSocket(_ipAddress.getPort());
		} catch (IOException e) {
			try {
				_requestSocket.close();
			} catch (IOException e1) {
				// Socket closing exception should not be handled.
			}
			e.printStackTrace();
		}
	}
	
	/**
	 * Creates a NameServer list from the input serverFile.
	 * reads the nameServers (in format xxx.xxx.xxx.xxx%port or localhost)
	 * from the file and enters them to the FileManager's _nameServers list.
	 * @param serverList the path of the serverList file.
	 */
	private void createNameServerList(String serverList){
		File serverListFile = new File(serverList);
		try {
			BufferedReader input = 
								new BufferedReader(new FileReader(serverListFile));
			String serverAddressLine;
			while ((serverAddressLine = input.readLine())!=null){
					this._nameServers.add(IPaddress.interpretLine(serverAddressLine));
			}
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			// should I ignore?
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			// probably can ignore
			e.printStackTrace();
		}
	}
	
	/**
	 * Checks if this FileManager knows the nameServer or not.
	 * @param nameServer the nameServer to be checked.
	 * @return true if knows the nameserver, false otherwise.
	 */
	public boolean knowsNameServer(IPaddress nameServer){
		synchronized (_nameServers) {
			for (IPaddress server: _nameServers){
				if (server.equals(nameServer)){
					return true;
				}
			}
			return false;
		}
	}
	
	/**
	 * Adds a new nameServer to the FileManager's Database.
	 * @param nameServer the nameServer to be added.
	 */
	public void addNameServer(IPaddress nameServer){
		synchronized (_nameServers) {
			_nameServers.add(nameServer);
		}
	}
	
	/**
	 * Creates a file-name TreeSet from the fileManager's directory.
	 * adds every file name from the FileManager's directory to its
	 * files TreeSet.
	 */
	private void createFilesTree(){
		File directory=new File(this._directory);
		String[] filesArray = directory.list();
		for (String fileName: filesArray){
			this._files.add(new UploadFile(fileName));
		}
	}
	
	/**
	 * Returns a deep copy of the FileManager's NameServers list.
	 * @return a deep copy of the FileManager's NameServers list.
	 */
	public LinkedList<IPaddress> getNameServerList(){
		LinkedList<IPaddress> NameServerList = new LinkedList<IPaddress>();
		synchronized (_nameServers) {
			// Creating a deep copy of the nameServers list
			for (IPaddress serverAddress : _nameServers){
				NameServerList.add(new IPaddress(serverAddress));
			}
		}
		return NameServerList;
	}
	
	/**
	 * Returns a copy of the FileManager's file-names list.
	 * @return a copy of the FileManager's file-names list.
	 */
	public TreeSet<String> getFileList(){
		TreeSet<String> fileList = new TreeSet<String>();
		synchronized (_files) {
			for (UploadFile fileName: _files){
				fileList.add(fileName.getName());
			}
		}
		
		return fileList;
	}

	/**
	 * Returns the path of this FileManager's directory.
	 * @return the path of this FileManager's directory.
	 */
	public String getDir(){
		return this._directory;
	}
	
	/**
	 * Returns a copy of this FileManager's IPaddress.
	 * @return a copy of this FileManager's IPaddress.
	 */
	public IPaddress getIPaddress(){
		return new IPaddress(this._ipAddress);
	}
	
	/**
	 * Checks whether this FileManager contains a file or not.
	 * @param fileName the fileName to be searched for.
	 * @return the UploadFile itself if FileManager contains it, null otherwise.
	 */
	public UploadFile contains(String fileName){
		synchronized (_files) {
			for (UploadFile file: _files){
				if (file.getName().equals(fileName)){
					return file;
				}
			}
		}
		return null;
	}

	//TODO check if necessary, might have been replaced by finishUploads
	/**
	 * Makes all this FileManager files un-uploadable.
	 * Will change the state of every file to Not uploadable.
	 */
	public void disableUploads(){
		synchronized (_files) {
			for (UploadFile file:_files){
				file.setNotUploadable();
			}
		}
	}
	
	/**
	 * Removes a file from the FileManagers DB.
	 * Will delete the file from the database.
	 * @param name the fileName to be removed.
	 */
	public void removeFromFileManager(String name) {
		synchronized (_files) {
			for (UploadFile dbFile : _files){
				if (dbFile.getName().equals(name)){
					_files.remove(dbFile);
					break;
				}
			}
		}
	}
	
	/**
	 * Removes a file from the directory.
	 * Will delete the file from the directory.
	 * @param name the fileName to be removed.
	 */
	public void removeFromDir(String name) {
		File directory=new File(_directory);
		File[] fileList = directory.listFiles();
		for (File file: fileList){
			if (file.getName().equals(name)){
				System.out.println("Trying to remove...");
				System.out.println("Will remove: "+file.getAbsolutePath());
				System.out.println(file.delete());
				break;
			}
		}
	}
	
	public void addFileToDatabase(String fileName){
		UploadFile newFile = new UploadFile(fileName);
		synchronized(_files){
			_files.add(newFile);
		}
	}
	
	/**
	 * Renames a file from the fileManager's Database.
	 * Renames the file in the directory,
	 * and adds the file with the new name to the Database
	 * (the old name is already deleted).
	 * will change the file's name at the directory & the FileManager's DB.
	 * @param oldName the file name to be renamed.
	 * @param newName the new name for the file.
	 */
	public void rename(String oldName, String newName) {
		File directory=new File(_directory);
		File[] fileList = directory.listFiles();
		for (File file: fileList){
			if (file.getName().equals(oldName)){
				String newFilePath = file.getAbsolutePath();
				//TODO change replace all to replace last only
				//otherwise - ab\dir\ab will do problems
				newFilePath = newFilePath.replaceAll(oldName, newName);
				file.renameTo(new File(newFilePath));
				break;
			}
		}
		synchronized (_files) {
			// adds a new UploadFile named newName - already deleted the old one.
			_files.add(new UploadFile(newName));
		}
	}
	
	/**
	 * Restarts the FileManager's request Socket.
	 * Will be used in case of timeout for example.
	 */
	public void restartRequestSocket(){
		try {
			this._requestSocket = new ServerSocket(this._ipAddress.getPort());
			this._requestSocket.setSoTimeout(TIMEOUT_5_SECONDS);
		} catch (IOException e) {
			// TODO Auto-generated catch block
		}
	}
	
	/** 
	 * Sets the FileManager to be Offline - closes its requests Socket.
	 * therefore, no new connections can be accepted.
	 */
	public void setOffline(){
		try {
			this._requestSocket.close();
		} catch (IOException e) {
			//TODO check exception
			// Assuming this exception can be ignored
		}
	}
	
	/**
	 * Returns the FileManager's request Socket.
	 * @return the FileManager's request Socket.
	 */
	public ServerSocket getRequestSocket(){
		return this._requestSocket;
	}
	
	/**
	 * Will finish all running uploads by this FileManager.
	 * Will join all Upload threads created by it, and return true when done.
	 * @return true when all uploads are finished.
	 */
	public boolean finishAllUploads(){
		synchronized (_uploads) {
			for (UploadThread uploader : _uploads){
				try {
					uploader.join();
					_uploads.remove(uploader); //removed dead threads
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					//Can be ignored.
					e.printStackTrace();
				}
			}
		}
		return true;
	}
	
	/**
	 * Will finish all running uploads of file "fileName" by this FileManager.
	 * Will join all Upload threads uploading fileName, and return true when done.
	 * @return true when all fileName uploads are finished.
	 */
	public boolean finishFileUpload(String fileName){
		synchronized (_uploads) {
			for (UploadThread uploader : _uploads){
				if (uploader.getFileName().equals(fileName)){
					try {
						uploader.join();
						_uploads.remove(uploader); // removes the dead thread
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						//Can be ignored.
						e.printStackTrace();
					}
				}
			}
		}
		return true;
	}
	
	/**
	 * Adds a new UploadThead to the FileManager's UploadThreads list.
	 * @param uploader the UploadThread to be added.
	 */
	public void addUploader (UploadThread uploader){
		//TODO check if needs to synch on uploader as well...
		synchronized (_uploads) {
			_uploads.add(uploader);
		}
	}
	
	/**
	 * Returns the absolute path of fileName (from the FileManagers directory).
	 * @param fileName the fileName to be searched for.
	 * @return the absolute path of the File named fileName.
	 */
	public String getFilePath(String fileName){
		String path=null;
		File directory = new File(_directory);
		File[] fileManagerFiles = directory.listFiles();
		for (File file : fileManagerFiles){
			if (file.getName().equals(fileName)){
				path = file.getAbsolutePath();
				break;
			}
		}
		return path;
	}
	
	//TODO REMOVE DEBUGGING METHOD
	public void printDB(){
		System.out.println("@@@    @@@    @@@");
		System.out.println("Printing Database");
		System.out.println();
		System.out.println("The directory: "+_directory);
		System.out.println("Known NameServers:");
		for (IPaddress a:_nameServers){
			System.out.println("  "+a.getIP()+"%"+a.getPort());
		}
		System.out.println();
		System.out.println("Known Files:");
		for (UploadFile a: _files){
			System.out.println("   "+a.getName());
		}
		System.out.println();
		System.out.println(" End Of Database");
		System.out.println("@@@    @@@    @@@");

	}
	
}
